package xyz.xpman.prisonerfitness.entity;

import android.annotation.SuppressLint;
import android.content.Context;
import android.database.Cursor;
import android.database.sqlite.SQLiteDatabase;

import androidx.annotation.Nullable;

import org.jetbrains.annotations.NotNull;

import java.util.ArrayList;
import java.util.Collections;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.function.Function;

import xyz.xpman.prisonerfitness.sqlite.DbHelper;
import xyz.xpman.prisonerfitness.util.ArrayUtil;


@SuppressWarnings("unused")
public class BaseModel<T extends BaseModel<T>> extends SqlLiteModel {
    public static final String ID = "id";
    public static final String EMPTY_STRING = "";

    /**
     * Map<className:String, attrsName:Set<attrName:String>>
     */
    private static final Map<@NotNull String, @NotNull TableInfo> attrsMapping = new HashMap<>();
    protected static SQLiteDatabase db = null;

    protected final TableInfo table;


    // init
    // - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -


    public static void init(@Nullable Context context) {
        db = new DbHelper(context).getReadableDatabase();
    }


    public static void bind(@NotNull TableInfo tableInfo) {

        attrsMapping.put(tableInfo.clazz.getName(), tableInfo);
    }


    public static class TableInfo{
        static final TableInfo EMPTY = new TableInfo("", Collections.emptyMap(), null);

        final String id;
        final String name;
        final String idClause;
        final String[] columns;
        final Map<String, Class<?>> attrs;
        final Class<? extends BaseModel<?>> clazz;


        public TableInfo(@NotNull String name,
                         @NotNull Map<@NotNull String, @NotNull Class<?>> attrs,
                         Class<? extends BaseModel<?>> clazz) {
            this(name, attrs, clazz, null);
        }

        public TableInfo(@NotNull String name,
                         @NotNull Map<@NotNull String, @NotNull Class<?>> attrs,
                         Class<? extends BaseModel<?>> clazz,
                         String id) {
            this.id = id;
            this.name = name;
            this.clazz = clazz;
            this.idClause = id + " = ?";
            this.columns = ArrayUtil.toStringArray(attrs.keySet());
            this.attrs = Collections.unmodifiableMap(new HashMap<>(attrs));
        }

    }

    // - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -


    @SuppressLint("NewApi")
    public BaseModel() {
        table = attrsMapping.getOrDefault(getClass().getName(), TableInfo.EMPTY);
    }


    // cast
    // - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -


/*
    private ContentValues toContentValue() {
        ContentValues cv = new ContentValues(table.attrs.size());

        for (Map.Entry<String, Object> entry : getColumns().entrySet()) {
            Class<?> clazz = table.attrs.get(entry.getKey());
            String value = entry.getValue().toString();

            if (clazz == String.class) {
                cv.put(entry.getKey(), value);
            } else if (clazz == Byte.class) {
                Byte val = Byte.valueOf(value);
                cv.put(entry.getKey(), val);
            } else if (clazz == Short.class) {
                Short val = Short.valueOf(value);
                cv.put(entry.getKey(), val);
            } else if (clazz == Integer.class) {
                Integer val = Integer.valueOf(value);
                cv.put(entry.getKey(), val);
            } else if (clazz == Long.class) {
                Long val = Long.valueOf(value);
                cv.put(entry.getKey(), val);
            } else if (clazz == Float.class) {
                Float val = Float.valueOf(value);
                cv.put(entry.getKey(), val);
            } else if (clazz == Double.class) {
                Double val = Double.valueOf(value);
                cv.put(entry.getKey(), val);
            } else if (clazz == Boolean.class) {
                Boolean val = Boolean.valueOf(value);
                cv.put(entry.getKey(), val);
            } else if (clazz == byte[].class) {
                byte[] val = value.getBytes();
                cv.put(entry.getKey(), val);
            }
        }
        return cv;
    }
*/

    /**
     * @param index check range
     */
    private Object toObject(@NotNull Class<?> clazz, @NotNull Cursor cursor, int index) {
        // Log.i("BaseModel", "toObject index: " + index);

        Object value = null;

        if (clazz == String.class) {
            value = cursor.getString(index);
        } else if (clazz == Short.class) {
            value = cursor.getShort(index);
        } else if (clazz == Integer.class) {
            value = cursor.getInt(index);
        } else if (clazz == Long.class) {
            value = cursor.getLong(index);
        } else if (clazz == Float.class) {
            value = cursor.getFloat(index);
        } else if (clazz == Double.class) {
            value = cursor.getDouble(index);
        }else if (clazz == byte[].class) {
            value = cursor.getBlob(index);
        }

        return value;
    }


    // - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -



    // - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -


    @SuppressWarnings("unckecked")
    protected T byCursor(@NotNull Cursor cursor) {
        BaseModel<?> obj = null;

        try {
            obj = table.clazz.newInstance();

            for (String colName : cursor.getColumnNames()) {
                final int index = cursor.getColumnIndex(colName);
                final Class<?> clazz = table.attrs.get(colName);

                if (0 <= index && index <= cursor.getColumnCount() && null != clazz) {
                    obj.setObject(colName, toObject(clazz, cursor, index));
                }
            }
        } catch (IllegalAccessException | InstantiationException e) {
            e.printStackTrace();
        }

        return (T) obj;
    }

    @SuppressLint("NewApi")
    protected T processCursor(Cursor cursor, Function<Cursor, T> func) {
        T obj = null;

        if (null != cursor && 1 == cursor.getCount()) {
            try {
                obj = func.apply(cursor);
            } finally {
                cursor.close();
            }
        }
        return obj;
    }

    @SuppressLint("NewApi")
    protected <M> M processCursorDiy(Cursor cursor, Function<Cursor, M> func) {
        M obj = null;

        if (null != cursor && 1 == cursor.getCount()) {
            try {
                obj = func.apply(cursor);
            } finally {
                cursor.close();
            }
        }
        return obj;
    }


    protected boolean isCursorEmpty(Cursor cursor) {
        return null != cursor && 0 < cursor.getCount();
    }



    protected @NotNull List<T> cursorToList(@NotNull Cursor cursor) {
        cursor.moveToFirst();

        final List<T> list = new ArrayList<>();

        while (!cursor.isAfterLast()) {
            list.add(byCursor(cursor));

            cursor.moveToNext();
        }

        return list.isEmpty() ? Collections.emptyList() : list;
    }


    // add, delete, update, find
    // - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -



    public boolean add() {
        return -1 != db.insert(table.name, null, attrs);
    }

    public boolean deleteById(String id) {
        return 1 == db.delete(table.name, table.idClause, new String[]{ id });
    }

    public boolean update() {
        return 1 == db.update(table.name, attrs, table.idClause,
                new String[]{ String.valueOf(attrs.get(table.id)) });
    }



    public T findById(String id) {
        return findById(id, "*");
    }

    protected T findById(String id, String attrs) {
        final String sqlFmt = "select %s from %s where %s";
        final String sql = String.format(sqlFmt,
                attrs, table.name, table.idClause);

        @SuppressLint("Recycle")
        Cursor cursor = db.rawQuery(sql, new String[]{ id });

        return processCursor(cursor, this::byCursor);
    }



    public List<T> findAll() {
        return findAll(table.columns);
    }

    protected List<T> findAll(String[] columns) {
        @SuppressLint("Recycle")
        Cursor cursor = db.query(table.name, columns,
                "", new String[0],"", "", "");

        return cursorToList(cursor);
    }



    // check
    // - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -



    public String checkForAdd() {
        return EMPTY_STRING;
    }

    public String checkForUpdate() {
        return EMPTY_STRING;
    }



    // attrs
    // - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -




    // attrs
    // - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -


    @SuppressLint("NewApi")
    public Set<String> getAttrs() {
        return table.attrs.keySet();
    }


    // toString
    // - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -


    @Override
    public @NotNull String toString() {
        return table.name + " {" + attrs.toString() + "} \n";
    }

}
